Object.assign(pc, function () {
    var id = 0;

    /**
     * @class
     * @name pc.Material
     * @classdesc A material determines how a particular mesh instance is rendered. It specifies the shader and render state that is
     * set before the mesh instance is submitted to the graphics device.
     * @description Create a new Material instance.
     * @property {number} alphaTest The alpha test reference value to control which fragments are written to the currently
     * active render target based on alpha value. All fragments with an alpha value of less than the alphaTest reference value
     * will be discarded. alphaTest defaults to 0 (all fragments pass).
     * @property {boolean} alphaToCoverage Enables or disables alpha to coverage (WebGL2 only). When enabled, and if hardware anti-aliasing is on,
     * limited order-independent transparency can be achieved. Quality depends on the number of MSAA samples of the current render target.
     * It can nicely soften edges of otherwise sharp alpha cutouts, but isn't recommended for large area semi-transparent surfaces.
     * Note, that you don't need to enable blending to make alpha to coverage work. It will work without it, just like alphaTest.
     * @property {boolean} alphaWrite If true, the alpha component of fragments generated by the shader of this material is written to
     * the color buffer of the currently active render target. If false, the alpha component will not be written. Defaults to true.
     * @property {number} blendType Controls how primitives are blended when being written to the currently active render target.
     * Can be:
     *
     * * {@link pc.BLEND_SUBTRACTIVE}: Subtract the color of the source fragment from the destination fragment and write the result to the frame buffer.
     * * {@link pc.BLEND_ADDITIVE}: Add the color of the source fragment to the destination fragment and write the result to the frame buffer.
     * * {@link pc.BLEND_NORMAL}: Enable simple translucency for materials such as glass. This is equivalent to enabling a source blend mode of pc.BLENDMODE_SRC_ALPHA and a destination blend mode of pc.BLENDMODE_ONE_MINUS_SRC_ALPHA.
     * * {@link pc.BLEND_NONE}: Disable blending.
     * * {@link pc.BLEND_PREMULTIPLIED}: Similar to pc.BLEND_NORMAL expect the source fragment is assumed to have already been multiplied by the source alpha value.
     * * {@link pc.BLEND_MULTIPLICATIVE}: Multiply the color of the source fragment by the color of the destination fragment and write the result to the frame buffer.
     * * {@link pc.BLEND_ADDITIVEALPHA}: Same as pc.BLEND_ADDITIVE except the source RGB is multiplied by the source alpha.
     *
     * Defaults to pc.BLEND_NONE.
     * @property {boolean} blueWrite If true, the blue component of fragments generated by the shader of this material is written to
     * the color buffer of the currently active render target. If false, the blue component will not be written. Defaults to true.
     * @property {number} cull Controls how triangles are culled based on their face direction with respect to the viewpoint.
     * Can be:
     *
     * * {@link pc.CULLFACE_NONE}: Do not cull triangles based on face direction.
     * * {@link pc.CULLFACE_BACK}: Cull the back faces of triangles (do not render triangles facing away from the view point).
     * * {@link pc.CULLFACE_FRONT}: Cull the front faces of triangles (do not render triangles facing towards the view point).
     * * {@link pc.CULLFACE_FRONTANDBACK}: Cull both front and back faces (triangles will not be rendered).
     *
     * Defaults to pc.CULLFACE_BACK.
     * @property {boolean} depthTest If true, fragments generated by the shader of this material are only written to the
     * current render target if they pass the depth test. If false, fragments generated by the shader of this material are
     * written to the current render target regardless of what is in the depth buffer. Defaults to true.
     * @property {boolean} depthWrite If true, fragments generated by the shader of this material write a depth value to
     * the depth buffer of the currently active render target. If false, no depth value is written. Defaults to true.
     * @property {boolean} greenWrite If true, the green component of fragments generated by the shader of this material is written to
     * the color buffer of the currently active render target. If false, the green component will not be written. Defaults to true.
     * @property {string} name The name of the material.
     * @property {boolean} redWrite If true, the red component of fragments generated by the shader of this material is written to
     * the color buffer of the currently active render target. If false, the red component will not be written. Defaults to true.
     * @property {pc.Shader|null} shader The shader used by this material to render mesh instances (default is null).
     * @property {pc.StencilParameters|null} stencilFront Stencil parameters for front faces (default is null).
     * @property {pc.StencilParameters|null} stencilBack Stencil parameters for back faces (default is null).
     * @property {number} depthBias Offsets the output depth buffer value. Useful for decals to prevent z-fighting.
     * @property {number} slopeDepthBias Same as {@link pc.Material#depthBias}, but also depends on the slope of the triangle relative to the camera.
     */
    var Material = function Material() {
        this.name = "Untitled";
        this.id = id++;

        this._shader = null;
        this.variants = {};
        this.parameters = {};

        // Render states
        this.alphaTest = 0;
        this.alphaToCoverage = false;

        this.blend = false;
        this.blendSrc = pc.BLENDMODE_ONE;
        this.blendDst = pc.BLENDMODE_ZERO;
        this.blendEquation = pc.BLENDEQUATION_ADD;

        this.separateAlphaBlend = false;
        this.blendSrcAlpha = pc.BLENDMODE_ONE;
        this.blendDstAlpha = pc.BLENDMODE_ZERO;
        this.blendAlphaEquation = pc.BLENDEQUATION_ADD;

        this.cull = pc.CULLFACE_BACK;

        this.depthTest = true;
        this.depthWrite = true;
        this.stencilFront = null;
        this.stencilBack = null;

        this.depthBias = 0;
        this.slopeDepthBias = 0;

        this.redWrite = true;
        this.greenWrite = true;
        this.blueWrite = true;
        this.alphaWrite = true;

        this.meshInstances = []; // The mesh instances referencing this material

        this._shaderVersion = 0;
        this._scene = null;
        this._dirtyBlend = false;

        this.dirty = true;
    };

    Object.defineProperty(Material.prototype, 'shader', {
        get: function () {
            return this._shader;
        },
        set: function (shader) {
            this._shader = shader;
        }
    });

    Object.defineProperty(Material.prototype, 'blendType', {
        get: function () {
            if ((!this.blend) &&
                (this.blendSrc === pc.BLENDMODE_ONE) &&
                (this.blendDst === pc.BLENDMODE_ZERO) &&
                (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_NONE;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_SRC_ALPHA) &&
                       (this.blendDst === pc.BLENDMODE_ONE_MINUS_SRC_ALPHA) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_NORMAL;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_ONE) &&
                       (this.blendDst === pc.BLENDMODE_ONE) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_ADDITIVE;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_SRC_ALPHA) &&
                       (this.blendDst === pc.BLENDMODE_ONE) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_ADDITIVEALPHA;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_DST_COLOR) &&
                       (this.blendDst === pc.BLENDMODE_SRC_COLOR) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_MULTIPLICATIVE2X;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_ONE_MINUS_DST_COLOR) &&
                       (this.blendDst === pc.BLENDMODE_ONE) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_SCREEN;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_ONE) &&
                       (this.blendDst === pc.BLENDMODE_ONE) &&
                       (this.blendEquation === pc.BLENDEQUATION_MIN)) {
                return pc.BLEND_MIN;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_ONE) &&
                       (this.blendDst === pc.BLENDMODE_ONE) &&
                       (this.blendEquation === pc.BLENDEQUATION_MAX)) {
                return pc.BLEND_MAX;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_DST_COLOR) &&
                       (this.blendDst === pc.BLENDMODE_ZERO) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_MULTIPLICATIVE;
            } else if ((this.blend) &&
                       (this.blendSrc === pc.BLENDMODE_ONE) &&
                       (this.blendDst === pc.BLENDMODE_ONE_MINUS_SRC_ALPHA) &&
                       (this.blendEquation === pc.BLENDEQUATION_ADD)) {
                return pc.BLEND_PREMULTIPLIED;
            }
            return pc.BLEND_NORMAL;
        },
        set: function (type) {
            var prevBlend = this.blend;
            switch (type) {
                case pc.BLEND_NONE:
                    this.blend = false;
                    this.blendSrc = pc.BLENDMODE_ONE;
                    this.blendDst = pc.BLENDMODE_ZERO;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_NORMAL:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_SRC_ALPHA;
                    this.blendDst = pc.BLENDMODE_ONE_MINUS_SRC_ALPHA;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_PREMULTIPLIED:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_ONE;
                    this.blendDst = pc.BLENDMODE_ONE_MINUS_SRC_ALPHA;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_ADDITIVE:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_ONE;
                    this.blendDst = pc.BLENDMODE_ONE;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_ADDITIVEALPHA:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_SRC_ALPHA;
                    this.blendDst = pc.BLENDMODE_ONE;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_MULTIPLICATIVE2X:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_DST_COLOR;
                    this.blendDst = pc.BLENDMODE_SRC_COLOR;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_SCREEN:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_ONE_MINUS_DST_COLOR;
                    this.blendDst = pc.BLENDMODE_ONE;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_MULTIPLICATIVE:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_DST_COLOR;
                    this.blendDst = pc.BLENDMODE_ZERO;
                    this.blendEquation = pc.BLENDEQUATION_ADD;
                    break;
                case pc.BLEND_MIN:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_ONE;
                    this.blendDst = pc.BLENDMODE_ONE;
                    this.blendEquation = pc.BLENDEQUATION_MIN;
                    break;
                case pc.BLEND_MAX:
                    this.blend = true;
                    this.blendSrc = pc.BLENDMODE_ONE;
                    this.blendDst = pc.BLENDMODE_ONE;
                    this.blendEquation = pc.BLENDEQUATION_MAX;
                    break;
            }
            if (prevBlend !== this.blend) {
                if (this._scene) {
                    this._scene.layers._dirtyBlend = true;
                } else {
                    this._dirtyBlend = true;
                }
            }
            this._updateMeshInstanceKeys();
        }
    });

    Material.prototype._cloneInternal = function (clone) {
        clone.name = this.name;
        clone.shader = this.shader;

        // Render states
        clone.alphaTest = this.alphaTest;
        clone.alphaToCoverage = this.alphaToCoverage;

        clone.blend = this.blend;
        clone.blendSrc = this.blendSrc;
        clone.blendDst = this.blendDst;
        clone.blendEquation = this.blendEquation;

        clone.separateAlphaBlend = this.separateAlphaBlend;
        clone.blendSrcAlpha = this.blendSrcAlpha;
        clone.blendDstAlpha = this.blendDstAlpha;
        clone.blendAlphaEquation = this.blendAlphaEquation;

        clone.cull = this.cull;

        clone.depthTest = this.depthTest;
        clone.depthWrite = this.depthWrite;
        clone.depthBias = this.depthBias;
        clone.slopeDepthBias = this.slopeDepthBias;
        if (this.stencilFront) clone.stencilFront = this.stencilFront.clone();
        if (this.stencilBack) {
            if (this.stencilFront === this.stencilBack) {
                clone.stencilBack = clone.stencilFront;
            } else {
                clone.stencilBack = this.stencilBack.clone();
            }
        }

        clone.redWrite = this.redWrite;
        clone.greenWrite = this.greenWrite;
        clone.blueWrite = this.blueWrite;
        clone.alphaWrite = this.alphaWrite;
    };

    Material.prototype.clone = function () {
        var clone = new pc.Material();
        this._cloneInternal(clone);
        return clone;
    };

    Material.prototype._updateMeshInstanceKeys = function () {
        var i, meshInstances = this.meshInstances;
        for (i = 0; i < meshInstances.length; i++) {
            meshInstances[i].updateKey();
        }
    };

    Material.prototype.updateUniforms = function () {
    };

    Material.prototype.updateShader = function (device, scene, objDefs) {
        // For vanilla materials, the shader can only be set by the user
    };

    /**
     * @function
     * @name pc.Material#update
     * @description Applies any changes made to the material's properties.
     */
    Material.prototype.update = function () {
        this.dirty = true;
    };

    // Parameter management
    Material.prototype.clearParameters = function () {
        this.parameters = {};
    };

    Material.prototype.getParameters = function () {
        return this.parameters;
    };

    Material.prototype.clearVariants = function () {
        var meshInstance;
        this.variants = {};
        var j;
        for (var i = 0; i < this.meshInstances.length; i++) {
            meshInstance = this.meshInstances[i];
            for (j = 0; j < meshInstance._shader.length; j++) {
                meshInstance._shader[j] = null;
            }
        }
    };

    /**
     * @function
     * @name pc.Material#getParameter
     * @description Retrieves the specified shader parameter from a material.
     * @param {string} name - The name of the parameter to query.
     * @returns {object} The named parameter.
     */
    Material.prototype.getParameter = function (name) {
        return this.parameters[name];
    };

    /**
     * @function
     * @name pc.Material#setParameter
     * @description Sets a shader parameter on a material.
     * @param {string} name - The name of the parameter to set.
     * @param {number|number[]|pc.Texture} data - The value for the specified parameter.
     * @param {number} [passFlags] - Mask describing which passes the material should be included in.
     */
    Material.prototype.setParameter = function (name, data, passFlags) {
        if (passFlags === undefined) passFlags = -524285; // All bits set except 2 - 18 range

        if (data === undefined && typeof name === 'object') {
            var uniformObject = name;
            if (uniformObject.length) {
                for (var i = 0; i < uniformObject.length; i++) {
                    this.setParameter(uniformObject[i]);
                }
                return;
            }
            name = uniformObject.name;
            data = uniformObject.value;
        }

        var param = this.parameters[name];
        if (param) {
            param.data = data;
            param.passFlags = passFlags;
        } else {
            this.parameters[name] = {
                scopeId: null,
                data: data,
                passFlags: passFlags
            };
        }
    };

    /**
     * @function
     * @name pc.Material#deleteParameter
     * @description Deletes a shader parameter on a material.
     * @param {string} name - The name of the parameter to delete.
     */
    Material.prototype.deleteParameter = function (name) {
        if (this.parameters[name]) {
            delete this.parameters[name];
        }
    };

    /**
     * @function
     * @name pc.Material#setParameters
     * @description Pushes all material parameters into scope.
     */
    Material.prototype.setParameters = function () {
        // Push each shader parameter into scope
        for (var paramName in this.parameters) {
            var parameter = this.parameters[paramName];
            parameter.scopeId.setValue(parameter.data);
        }
    };

    /**
     * @function
     * @name pc.Material#destroy
     * @description Removes this material from the scene and possibly frees up memory from its shaders (if there are no other materials using it).
     */
    Material.prototype.destroy = function () {
        this.variants = {};
        this.shader = null;

        var meshInstance, j;
        for (var i = 0; i < this.meshInstances.length; i++) {
            meshInstance = this.meshInstances[i];
            for (j = 0; j < meshInstance._shader.length; j++) {
                meshInstance._shader[j] = null;
            }
            meshInstance._material = null;
            var defaultMaterial = pc.getDefaultMaterial();
            if (this !== defaultMaterial) {
                meshInstance.material = defaultMaterial;
            }
        }
    };

    return {
        Material: Material
    };
}());
